import { Writable } from 'node:stream';

import { beforeEach, describe, expect, it, MockedObject } from 'vitest';

import { createMockInstance } from './__fixtures__/create-mock-instance.js';
import { createFakeCloseEvent, FakeCommand } from './__fixtures__/fake-command.js';
import { OutputWriter } from './output-writer.js';

let outputStream: MockedObject<Writable>;
let commands: FakeCommand[];

function createWriter(overrides?: { group: boolean }) {
    const options = {
        outputStream,
        group: false,
        commands,
        ...overrides,
    };
    return new OutputWriter(options);
}

function closeCommand(command: FakeCommand) {
    command.state = 'exited';
    command.close.next(createFakeCloseEvent({ command, index: command.index }));
}

beforeEach(() => {
    outputStream = createMockInstance(Writable);
    commands = [
        new FakeCommand('', undefined, 0),
        new FakeCommand('', undefined, 1),
        new FakeCommand('', undefined, 2),
    ];
});

it('throws if outputStream already is in errored state', () => {
    Object.defineProperty(outputStream, 'errored', { value: new Error('test') });
    expect(() => createWriter()).toThrow(TypeError);
});

describe('#write()', () => {
    it('throws if outputStream has errored', () => {
        const writer = createWriter();
        Object.defineProperty(outputStream, 'errored', { value: new Error('test') });
        expect(() => writer.write(commands[0], 'hello')).toThrow(TypeError);
    });

    describe('with group=false', () => {
        it('writes instantly', () => {
            const writer = createWriter({ group: false });
            writer.write(commands[2], 'hello');
            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');
        });
    });

    describe('with group=true', () => {
        it('writes for null commands', () => {
            const writer = createWriter({ group: true });
            writer.write(undefined, 'hello');
            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');
        });

        it('does not write instantly for non-active command', () => {
            const writer = createWriter({ group: true });
            writer.write(commands[2], 'hello');
            expect(outputStream.write).toHaveBeenCalledTimes(0);
            expect(writer.buffers[2]).toEqual(['hello']);
        });

        it('write instantly for active command', () => {
            const writer = createWriter({ group: true });
            writer.write(commands[0], 'hello');
            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');
        });

        it('does not wait for write from next command to flush', () => {
            const writer = createWriter({ group: true });
            writer.write(commands[1], 'hello');
            writer.write(commands[1], 'foo bar');
            expect(outputStream.write).toHaveBeenCalledTimes(0);
            closeCommand(commands[0]);
            expect(outputStream.write).toHaveBeenCalledTimes(2);
            expect(writer.activeCommandIndex).toBe(1);
            outputStream.write.mockClear();

            writer.write(commands[1], 'blah');
            expect(outputStream.write).toHaveBeenCalledTimes(1);
        });

        it('does not flush for non-active command', () => {
            const writer = createWriter({ group: true });
            writer.write(commands[1], 'hello');
            writer.write(commands[1], 'foo bar');
            expect(outputStream.write).toHaveBeenCalledTimes(0);
            closeCommand(commands[1]);
            expect(outputStream.write).toHaveBeenCalledTimes(0);
            closeCommand(commands[0]);
            expect(outputStream.write).toHaveBeenCalledTimes(2);
        });

        it('flushes multiple commands at a time if necessary', () => {
            const writer = createWriter({ group: true });
            writer.write(commands[2], 'hello');
            closeCommand(commands[1]);
            closeCommand(commands[2]);
            expect(outputStream.write).toHaveBeenCalledTimes(0);
            closeCommand(commands[0]);
            expect(outputStream.write).toHaveBeenCalledExactlyOnceWith('hello');
            expect(writer.activeCommandIndex).toBe(2);
        });
    });
});
